2-4 React核心概念(二):协调概念以及Diff算法
什么是协调(Reconciliation)
前面我们了解到,JSX 通过 React.createElement() 转换为 JavaScript 对象,形成一棵组件树。这棵组件树就是虚拟 DOM——一个存放在内存中的、用 JavaScript 对象描述的 DOM 结构。
React 之所以被称为"革命性的",关键就在于它将传统的"直接操作真实 DOM"变成了"通过 JavaScript 高速操作虚拟 DOM"。JavaScript 的执行速度远快于浏览器操作真实 DOM 的速度,这就是 React 性能优势的根本来源。
但虚拟 DOM 不是一成不变的——每当状态(state)或属性(props)变化时,React 会生成一棵新的虚拟 DOM 树。此时面临的核心问题是:如何高效地将新虚拟 DOM 同步到真实 DOM?
这就是**协调(Reconciliation)**要解决的问题。
Diff 算法:从 O(n³) 到 O(n)
朴素方案的困境
两棵树进行完整对比的时间复杂度是 O(n³)。什么概念?如果有 1000 个元素,需要进行的比较次数是 1000 × 1000 × 1000 = 10 亿次。这个开销在任何实际应用中都是不可接受的。
React 的 Diff 策略
React 团队基于两个前提假设,将 Diff 算法的复杂度降到了 O(n):
- 不同类型的元素会产生不同的树——例如
<div>变成<span>,React 直接销毁旧树、创建新树 - 开发者通过
keyprop 标识哪些子元素在不同渲染中是稳定的
这意味着 1000 个元素只需要 1000 次比较,性能提升达到百万倍。
Diff 算法的三条规则
规则一:不同类型的元素
当根节点的 type 不同时,React 会直接销毁旧树、重建新树:
// 旧
<h1>Title 1</h1>
// 新
<h2>Title 2</h2>
jsx
React 会销毁 <h1> 及其子节点,创建 <h2> 及其子节点。不会尝试复用。
// 旧
<div className="old">Content</div>
// 新
<span className="new">Content</span>
jsx
同样,<div> 变成 <span> 会触发完全重建。
规则二:相同类型的元素
当 type 相同时,React 只更新变化的属性:
// 旧
<div className="old" id="box">Content</div>
// 新
<div className="new" id="box">Content</div>
jsx
React 只更新 className 属性,DOM 节点实例保持不变。这种按需更新避免了不必要的 DOM 操作。
规则三:列表元素的 key
这是 Diff 算法中最重要也最容易被忽视的规则。来看一个具体案例:
在列表尾部添加元素(无 key):
// 旧
<li>Apple</li>
<li>Banana</li>
// 新
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
jsx
React 逐个比较,前两个相同不更新,只添加第三个。性能表现良好。
在列表头部插入元素(无 key):
// 旧
<li>Apple</li>
<li>Banana</li>
// 新
<li>Cherry</li> // 新插入
<li>Apple</li>
<li>Banana</li>
jsx
React 逐个比较发现:
- 第1个
<li>内容从 "Apple" 变成了 "Cherry" → 更新 - 第2个
<li>内容从 "Banana" 变成了 "Apple" → 更新 - 第3个
<li>是新增的 → 创建
结果是所有元素都被更新了,而实际上只需要在头部插入一个新元素。
key 的正确使用方式
给每个元素添加唯一的 key 属性后:
// 旧
<li key="a">Apple</li>
<li key="b">Banana</li>
// 新
<li key="c">Cherry</li> // 新插入
<li key="a">Apple</li> // key="a",无需更新
<li key="b">Banana</li> // key="b",无需更新
jsx
React 通过 key 识别出 "Apple" 和 "Banana" 没有变化,只是位置移动了,只需在头部插入 "Cherry"。性能表现最优。
为什么不能用 index 作为 key
// 使用 index 作为 key(错误做法)
items.map((item, index) => <li key={index}>{item.name}</li>);
jsx
当在头部插入元素时,所有元素的 index 都会加 1。React 发现所有 key 都变了,会认为所有元素都是新的,触发全量更新——这和使用无 key 的效果一样。
正确做法是使用数据中的唯一标识:
// 使用唯一 ID 作为 key(正确做法)
items.map(item => <li key={item.id}>{item.name}</li>);
jsx
Diff 算法优化效果对比
| 场景 | 无 key / index key | 唯一 key |
|---|---|---|
| 尾部添加 | 只更新新增元素 | 只更新新增元素 |
| 头部插入 | 全部元素重新渲染 | 只插入新元素 |
| 中间插入 | 大量元素重新渲染 | 只插入新元素 |
| 排序 | 全部元素重新渲染 | 只调整位置 |
协调的工作流程总结
状态/属性变化
↓
生成新的虚拟 DOM 树(JavaScript 对象)
↓
Diff 算法对比新旧虚拟 DOM
↓ (O(n) 复杂度)
计算最小差异(按需更新)
↓
进入渲染阶段(Rendering)
↓
将差异同步到真实 DOM
text
Diff 算法的本质是在正式操作真实 DOM 之前,先用 JavaScript 做一次高效的计算优化,找出最小变更集。这样真正需要操作真实 DOM 的次数就被压缩到了最低。
下一节将进入渲染(Rendering)阶段,了解 React 如何将 Diff 计算出的差异同步到真实 DOM,以及 React 15 与 React 16 在渲染机制上的根本性变化。
↑